Effective Java 2.0_中英文对照_Item 6

文章作者:Tyan
博客:noahsnail.com | CSDN | 简书

Item 6: Eliminate obsolete object references

When you switch from a language with manual memory management, such as C or C++, to a garbage-collected language, your job as a programmer is made much easier by the fact that your objects are automatically reclaimed when you’re through with them. It seems almost like magic when you first experience it. It can easily lead to the impression that you don’t have to think about memory management, but this isn’t quite true.

当你从一个手动管理内存的语言(例如C或C++)转到一个具有垃圾回收机制的语言时,作为一个程序员你的工作会更容易,当你使用完对象时,它们会被自动回收。当你第一个经历它时,它简直不可思议。它很容易给你留下一个你不需要考虑内存管理的印象,但事实并非如此。

Consider the following simple stack implementation:

考虑下面一种简单的栈实现的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Can you spot the "memory leak"?
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;

public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}

public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}

public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}

/**
* Ensure space for at least one more element, roughly doubling the capacity
* each time the array needs to grow.
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}

There’s nothing obviously wrong with this program (but see Item 26 for a generic version). You could test it exhaustively, and it would pass every test with flying colors, but there’s a problem lurking. Loosely speaking, the program has a “memory leak,” which can silently manifest itself as reduced performance due to increased garbage collector activity or increased memory footprint. In extreme cases, such memory leaks can cause disk paging and even program failure with an OutOfMemoryError, but such failures are relatively rare.

这个程序没有明显的错误(但请看Item 26的泛型版本)。你可以对它进行全面测试,它能出色的通过每一次测试,但这儿有一个潜在的问题。不严格的说,这个程序有一个『内存泄露』问题,由于垃圾回收活动的增加或内存占用的增加,性能下降的情况会逐渐表现出来。在极端的情况下,这种内存泄露可能引起磁盘分页,甚至会引起程序失败(OutOfMemoryError),但这种失败是相对稀少的。

So where is the memory leak? If a stack grows and then shrinks, the objects that were popped off the stack will not be garbage collected, even if the program using the stack has no more references to them. This is because the stack maintains obsolete references to these objects. An obsolete reference is simply a reference that will never be dereferenced again. In this case, any references outside of the “active portion” of the element array are obsolete. The active portion consists of the elements whose index is less than size.

内存泄露在哪呢?如果栈先增长后收缩,出栈的对象将不能作为垃圾被收回,即使使用栈的程序不再引用它们。这是因为栈维护着这些对象的废弃引用。废弃引用是永远不会再解引用的引用。在这种情况下,元素数组活跃部分之外的其它引用都将被废弃。活跃部分包含了那些索引小于size的元素。

Memory leaks in garbage-collected languages (more properly known as unintentional object retentions) are insidious. If an object reference is unintentionally retained, not only is that object excluded from garbage collection, but so too are any objects referenced by that object, and so on. Even if only a few object references are unintentionally retained, many, many objects may be prevented from being garbage collected, with potentially large effects on performance.

内存泄露在垃圾回收语言是隐蔽的(更合适的称呼是无意识对象保持)。如果一个对象引用被无意保留,不仅这个对象不能被垃圾回收处理,而且这个对象引用的其它对象也不能被垃圾回收处理,以此类推。即使只无意保留了几个对象的引用,但可能阻止了垃圾回收机制回收许多其它的对象,在性能上会有很大的潜在影响。

The fix for this sort of problem is simple: null out references once they become obsolete. In the case of our Stack class, the reference to an item becomes obsolete as soon as it’s popped off the stack. The corrected version of the pop method looks like this:

这类问题的修正很简单:一旦对象引用过期,就清空这些引用。在我们的Stack类例子中,只要某一项从栈中取出,它的引用就废弃了。pop方法的修正版本如下:

1
2
3
4
5
6
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference return result;
}

An added benefit of nulling out obsolete references is that, if they are subsequently dereferenced by mistake, the program will immediately fail with a NullPointerException, rather than quietly doing the wrong thing. It is always beneficial to detect programming errors as quickly as possible.

清空废弃引用的一个额外收益是,如果它们接下来被误解引用,程序会立刻抛出NullPointerException,而不是静静地做错误的事情。对于尽可能快的检测程序错误,它总是有益的。

When programmers are first stung by this problem, they may overcompensate by nulling out every object reference as soon as the program is finished using it. This is neither necessary nor desirable, as it clutters up the program unnecessarily. Nulling out object references should be the exception rather than the norm. The best way to eliminate an obsolete reference is to let the variable that contained the reference fall out of scope. This occurs naturally if you define each variable in the narrowest possible scope (Item 45).

当程序员第一次被这个问题困扰时,他们可能是过分小心了,程序一旦完成了对象的使用,就清空每一个对象的引用。这既没必要也不可取,因此它会将程序不必要的弄乱。清空对象引用应该是例外情况而不是正常的行为。消除废弃引用的最好方式是让包含引用的变量结束其作用域。如果你在最紧凑的作用域范围内定义每个变量,这会很自然的发生。

So when should you null out a reference? What aspect of the Stack class makes it susceptible to memory leaks? Simply put, it manages its own memory. The storage pool consists of the elements of the elements array (the object reference cells, not the objects themselves). The elements in the active portion of the array (as defined earlier) are allocated, and those in the remainder of the array are free. The garbage collector has no way of knowing this; to the garbage collector, all of the object references in the elements array are equally valid. Only the programmer knows that the inactive portion of the array is unimportant. The programmer effectively communicates this fact to the garbage collector by manually nulling out array elements as soon as they become part of the inactive portion.

你应该什么时候清空一个引用?Stack类的哪一个方面让它容易受到内存泄露影响?简单的说,它自己管理自己的内存存储池包含了元素数组中的元素(对象引用单元,不是对象本身)。数组活跃部分的元素(前面定义的)被分配,数组中其余的元素是自由的。垃圾回收器不知道这种情况;对于垃圾回收器而言,元素数组中的所有对象引用都同等有效。只有程序员知道数组中非活跃部分是不重要的。程序员通过手动清空数组元素中不活跃的部分,可以有效的告诉垃圾回收器这个事实。

Generally speaking, whenever a class manages its own memory, the programmer should be alert for memory leaks. Whenever an element is freed, any object references contained in the element should be nulled out.

一般来说,只要一个类自己管理自己的内存,程序员就应该警惕内存泄露。无论什么时候释放一个元素,这个元素包含的对象引用都应该被清空。

Another common source of memory leaks is caches. Once you put an object reference into a cache, it’s easy to forget that it’s there and leave it in the cache long after it becomes irrelevant. There are several solutions to this problem. If you’re lucky enough to implement a cache for which an entry is relevant exactly so long as there are references to its key outside of the cache, represent the cache as a WeakHashMap; entries will be removed automatically after they become obsolete. Remember that WeakHashMap is useful only if the desired lifetime of cache entries is determined by external references to the key, not the value.

另一个常见的内存泄露来源是缓存。一旦你把一个对象引用放入缓存,很容易忘了它在缓存中,在用完之后很长一段时间仍把它放在缓存中。这个问题有几种解决方案。如果你很幸运的要实现一个对于输入项的缓存,只要缓存外部有输入项的键的引用,它就是相对确定的,可以用一个WeakHashMap来表示缓存;在输入项废弃之后,它们会被自动移除。记住,只有缓存输入项的生命周期由输入项键的外部引用决定,不是由输入项值的外部引用决定时,WeakHashMap才有用的。

More commonly, the useful lifetime of a cache entry is less well defined, with entries becoming less valuable over time. Under these circumstances, the cache should occasionally be cleansed of entries that have fallen into disuse. This can be done by a background thread (perhaps a Timer or ScheduledThreadPoolExecutor) or as a side effect of adding new entries to the cache. The LinkedHashMap class facilitates the latter approach with its removeEldestEntry method. For more sophisticated caches, you may need to use java.lang.ref directly.

更常见的是,缓存输入项的有效生命周期是不太好定义的,随时间推移缓存输入项变的更没价值。在这些情况下,缓存应该时不时的清除停止使用的缓存输入项。这项工作可以通过一个后台线程去做(可能是一个TimerScheduledThreadPoolExecutor)或在新的输入项添加到缓存中时顺便去做。LinkedHashMap类利用它的removeEldestEntry方法可以很容易实现后面的方法。对于更复杂的缓存,你可能需要使用java.lang.ref directly

A third common source of memory leaks is listeners and other callbacks.If you implement an API where clients register callbacks but don’t deregister them explicitly, they will accumulate unless you take some action. The best way to ensure that callbacks are garbage collected promptly is to store only weak references to them, for instance, by storing them only as keys in a WeakHashMap.

第三个常见的内存泄露来源是监听器和其它的回调函数。如果你实现一个API,它的客户端注册了回调函数但没有显式的注销它们,除非你采取一些动作,否则它们将累积。确保回调函数可以迅速被垃圾回收的最好方式是为存储它们的弱引用,例如,只将它们保存为WeakHashMap的键。

Because memory leaks typically do not manifest themselves as obvious failures, they may remain present in a system for years. They are typically discovered only as a result of careful code inspection or with the aid of a debugging tool known as a heap profiler. Therefore, it is very desirable to learn to anticipate problems like this before they occur and prevent them from happening.

因为通常内存泄露没有明白的失败来揭露它们,它们可能在系统中存在许多年。通常只有通过小心的代码检查或通过调试工具(通常被称为堆分析器)的帮助才能发现它们。因此,在它们发生和阻止它们发生之前,就学习预测这种问题是很有必要的。

如果有收获,可以请我喝杯咖啡!